//
// Created by LiuYou on 2021/8/15.
//

#include <cstdlib>
#include <fcntl.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cerrno>
#include <arpa/inet.h>
#include <cassert>


/**
 * @brief 将文件描述符设置为非阻塞的。
 * @param fd
 * @return
 */
static int setNonblocking(int fd) {
    int oldOption = fcntl( fd, F_GETFL );
    int newOption = oldOption | O_NONBLOCK;
    fcntl( fd, F_SETFL, newOption );
    return oldOption;
}


/**
 * @brief 将文件描述符 fd 上的 EPOLLIN 事件注册到 epollFd 指定的epoll内核事件表中。
 * @param epollFd
 * @param fd
 * @param enableET 指定是否对 fd 启用 ET模式。
 */
static void addEventToFd(int epollFd, int fd, bool enableET) {
    epoll_event epollEvent{};
    epollEvent.data.fd = fd;
    epollEvent.events = EPOLLIN;
    if (enableET) {
        epollEvent.events |= EPOLLET;
    }
    epoll_ctl( epollFd, EPOLL_CTL_ADD, fd, &epollEvent );
    setNonblocking( fd );
}

/**
 * @brief LT模式的工作流程。
 * @param epollEvent 
 * @param number 
 * @param epollFd 
 * @param listenFd 
 */
static void LT(epoll_event* epollEvent, int number, int epollFd, int listenFd) {
    char buffer[10];
    for (int i = 0; i < number; ++i) {
        int sockFd = epollEvent[i].data.fd;
        if (sockFd == listenFd) {
            sockaddr_in remoteAddress{};
            socklen_t socklen = sizeof( remoteAddress );
            int connectFd = accept( listenFd, (sockaddr*) &remoteAddress, &socklen );
            addEventToFd( epollFd, connectFd, false );
        } else if (epollEvent[i].events & EPOLLIN) {
            printf( "event trigger once.(事件触发一次。)\n" );
            fflush( stdout );
            memset( buffer, '\0', sizeof( buffer ));
            /*
             * todo 笔记: 这里如果是客户一下子发的内容大于了 9 个字节, 服务器是一次recv不完的, 服务器是
             *  会多次在 epoll_wait()调用返回, 因为 与客户通信的服务器socket文件描述符中还有用户的数据,
             *  服务器要读完, 此时文件描述符才会不是就绪态。这其中 recv()函数执行了多次来读客户一次发来的
             *  内容。这就是lt模式。 一个epoll事件被重复触发的次数多(即多次调用epoll_wait()以及recv()函数等,
             *  直到这个事件处理完)。所以效率低。
             *
             * todo 笔记: 如果内容不是英文, 是中文的话, 可能还有出现乱码, 因为每次只能9个字节, 9个字节的读。
             *  英文一个字母就是一个字节, 而汉字是多字节, 如果在 9字节断开, 可能是正好从一个字的多字节中断开的,
             *  这样的话, 就会出现乱码！！！！
             *  (关于汉字, 在修正一下。刚刚我又调试发现, 如果telnet放松的内容都是汉字的话, 标点符号也是汉字,
             *  这样是不会出现乱码的。但是调试过程中如果telnet发送的是汉字加英文的标点符号, 就会出现乱码！！)
             *  另外, 在命令行敲下回车键, 这个回车键也是会被发送到服务器这边来的, 占两个字节。空格占一个字节。
             *  (关于汉字, 在补充一下, 只能是完全是汉字以及汉字的标点符号。而且不能混有数字、字母、以及英文的标点符号,
             *  这样的汉字流是不会出现乱码的, 因为我限制了是9个字节, 而刚刚好3个字节一个中文汉字, 所以如果是ASCII码
             *  为3个连续的, 这样中文就不会乱码了。)
             *
             *  todo 笔记: 所以, 缓冲区接受的字节数确实影响了对字符的解析。所以我分别修改为了 20 100 110, 发现了
             *   如果客户发送的内容的字节数是在 buffer之内的话, printf()函数打印是不会出现乱码的。而一旦超过
             *   了 buffer的大小的字节, 如果要 printf()函数打印的话, 如果汉字在 buffer的倒数第二个字节即 '\0'之前
             *   能解析出来一个汉字, 就不会出现乱码, 如果刚好是一个汉字的多字节(3个字节)的里面的某个字节(比如, 第2个字节)
             *   是 buffer大小的倒数第二个字节, 那么printf()函数打印出的结果就会在最后出现乱码！！！！
             *   防止乱码的话就要对流进行正确的解析, 将这些字节流拼接在一起。然后打印。
             */
            ssize_t ret = recv( sockFd, buffer, 10 - 1, 0 );
            if (ret <= 0) {
                close( sockFd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buffer );
            fflush( stdout );
        } else {
            printf( "something else happened(发生了其它的事情)\n" );
            fflush( stdout );
        }
    }
}


/**
 * @brief ET模式的工作流程。
 * @param epollEvent
 * @param number
 * @param epollFd
 * @param listenFd
 */
static void ET(epoll_event* epollEvent, int number, int epollFd, int listenFd) {
    char buffer[10];
    for (int i = 0; i < number; ++i) {
        int sockFd = epollEvent[i].data.fd;
        if (sockFd == listenFd) {
            sockaddr_in remoteAddress{};
            socklen_t socklen = sizeof( remoteAddress );
            int connectFd = accept( listenFd, (sockaddr*) &remoteAddress, &socklen );
            addEventToFd( epollFd, connectFd, true );
        } else if (epollEvent[i].events & EPOLLIN) {
            printf( "event trigger once(事件触发一次)\n" );
            fflush( stdout );
            /*
             * todo 笔记: 这里不是lt模式, 而是et模式。 因为这里有一个循环。et模式在很大程度上
             *  降低了同一个epoll重复被重复触发的次数, 因此效率要比tl模式高。
             */
            while (true) {
                memset( buffer, '\0', sizeof( buffer ));
                ssize_t ret = recv( sockFd, buffer, 10 - 1, 0 );
                if (ret < 0) {
                    if ((errno == EAGAIN) || errno == EWOULDBLOCK) {
                        printf( "读完了socketFd中的内容, socketFd的就绪态结束了。\n" );
                        fflush( stdout );
                        break;
                    }
                    close( sockFd );
                    break;
                } else if (ret == 0) {
                    close( sockFd );
                } else {
                    printf( "get %d bytes of content: %s\n", ret, buffer );
                    fflush( stdout );
                }
            }

        } else {
            printf( "something else happened\n" );
            fflush( stdout );
        }
    }
}

/**
 * @brief LT和ET模式。
 * @param argc
 * @param argv
 * @return
 */
int main(int argc, char** argv) {
    if (argc < 3) {
        printf( "用法: %s ip port\n", basename( argv[0] ));
        exit( 1 );
    }
    char* ip = argv[1];
    int port = atoi( argv[2] );


    sockaddr_in localAddress{};
    localAddress.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &localAddress.sin_addr );
    localAddress.sin_port = htons( port );

    int listenFd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenFd >= 0 );


    // 处理异常终止, 端口没有立刻释放的问题。这样就可以下一次继续bind()这个端口。
    int optVal = 1;
    if (setsockopt( listenFd, SOL_SOCKET, SO_REUSEADDR, &optVal, sizeof( optVal )) == -1) {
        perror( "setsockopt()" );
        exit( 1 );
    }


    int ret = bind( listenFd, (sockaddr*) &localAddress, sizeof( localAddress ));
    assert( ret != -1 );

    ret = listen( listenFd, 5 );
    assert( ret != -1 );

    epoll_event epollEvent[1024];
    int epollFd = epoll_create( 5 );
    assert( epollFd != -1 );
    // 将文件描述符 listenFd 上的 EPOLLIN 事件注册到 epollFd 指定的epoll内核事件表中,
    // 并将 listenFd设置为非阻塞。
    addEventToFd( epollFd, listenFd, true );

    while (true) {
        /*
         * todo 笔记: 调试 epoll_wait()函数时, 如果该执行这一行代码时, 不点下一步, 然后
         *  在centos 命令行中执行多次 telnet 192.168.52.128 54321 或者在已经连接到
         *  该服务器程序的 telnet中发送数据。
         *  然后, 再点调试下一步。这时epoll_wait()返回值会是 epoll监听的文件描述符为就绪态的个数(文件描述符就绪的个数)。
         *  即这种调试 epoll_wait()监听了多个感兴趣的文件描述符都变为了就绪态。
         *  程序会执行知道把 刚刚不点下一步之前做的所有客户的事件做完, 此时到 在调试到 epoll_wait()时, 它才会阻塞。
         *
         * todo 笔记: 另一种调试是, 执行这一行代码时, 点下一步, 因为 epoll_wait()本身是阻塞的,
         *  所以, 该服务端程序会阻塞在这里, 然后这时, 执行 telnet 192.168.52.128 54321 或者是epoll感兴趣监听的事件发生,
         *  epoll_wait()的返回值只会是 1, 因为你感兴趣的事件发生后, epoll_wait()由阻塞变为执行结束。
         *
         * todo 笔记: 这时两种调试阻塞的方式。这时我在自己调试中总结出来的。
         *
         */
        int ret = epoll_wait( epollFd, epollEvent, 1024, -1 );
        if (ret < 0) {
            printf( "epoll failure\n" );
            fflush( stdout );
            break;
        }
        LT( epollEvent, ret, epollFd, listenFd );
//        ET( epollEvent, ret, epollFd, listenFd );

    }

    close( listenFd );


    exit( 0 );
}